Fix Slack integration event context reads#154
Conversation
|
Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI. |
|
Warning Review limit reached
More reviews will be available in 44 minutes and 59 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughThis PR improves Slack integration event handling by introducing targeted context path selection based on subscription-matched mount paths, making DM listening opt-in rather than default, and adding infrastructure to mount and surface live thread context roots to agents. ChangesSlack Integration Enhancements
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request updates the Slack integration event bridge to resolve event context through mounted slug aliases when raw-id targeted reads fail, and introduces mounting for Slack thread context roots in mirror mode when historical downloads are disabled. Additionally, Slack direct message event scoping is now opt-in by default. Feedback suggests optimizing the retry loop for event context preview reads by skipping further retries on candidate paths that encounter permanent authorization errors, and generalizing the Slack provider check to support custom or enterprise Slack provider variants.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| for (const [index, delayMs] of readDelays.entries()) { | ||
| if (delayMs > 0) await delay(delayMs) | ||
| try { | ||
| return eventContextPreviewFromFile(await client.readFile(handle.workspaceId, path)) | ||
| } catch (error) { | ||
| readFileError = error | ||
| if (index === readDelays.length - 1) break | ||
| for (const candidatePath of candidatePaths) { | ||
| try { | ||
| return eventContextPreviewFromFile(await client.readFile(handle.workspaceId, candidatePath)) | ||
| } catch (error) { | ||
| readFileError = error | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
If a candidate path fails with a permanent error like 403 Forbidden or 401 Unauthorized (which can happen if the raw channel ID path is outside the allowed pathScope), retrying it in subsequent delay iterations is wasteful and increases latency. We should filter out candidate paths that encounter unauthorized/forbidden errors so they aren't retried in the next rounds.
let activeCandidates = [...candidatePaths]
for (const [index, delayMs] of readDelays.entries()) {
if (activeCandidates.length === 0) break
if (delayMs > 0) await delay(delayMs)
const nextCandidates: string[] = []
for (const candidatePath of activeCandidates) {
try {
return eventContextPreviewFromFile(await client.readFile(handle.workspaceId, candidatePath))
} catch (error) {
readFileError = error
if (!isUnauthorizedError(error)) {
nextCandidates.push(candidatePath)
}
}
}
activeCandidates = nextCandidates
}| function slackThreadContextMountPathsForIntegration(integration: ConnectedIntegration): string[] { | ||
| if (toRelayfileProvider(integration.provider) !== 'slack') return [] | ||
| return canonicalMountPathsForConnectedIntegration(integration) | ||
| .filter(isNarrowHistoricalMountPath) | ||
| .flatMap((mountPath) => { | ||
| const segments = mountPath.split('/').filter(Boolean) | ||
| if (segments[0] !== 'slack') return [] | ||
| const collection = segments[1] | ||
| if (!['channels', 'dms', 'users'].includes(collection || '')) return [] | ||
| if (segments.length === 3) return [`${mountPath}/threads`] | ||
| return [] | ||
| }) | ||
| } |
There was a problem hiding this comment.
The current implementation hardcodes the provider check to exactly 'slack'. If the repository uses enterprise or custom Slack provider variants (e.g., 'slack-enterprise'), this check and the subsequent segments[0] !== 'slack' check will fail, preventing thread context paths from being mounted. We should generalize this to support any provider starting with 'slack-'.
| function slackThreadContextMountPathsForIntegration(integration: ConnectedIntegration): string[] { | |
| if (toRelayfileProvider(integration.provider) !== 'slack') return [] | |
| return canonicalMountPathsForConnectedIntegration(integration) | |
| .filter(isNarrowHistoricalMountPath) | |
| .flatMap((mountPath) => { | |
| const segments = mountPath.split('/').filter(Boolean) | |
| if (segments[0] !== 'slack') return [] | |
| const collection = segments[1] | |
| if (!['channels', 'dms', 'users'].includes(collection || '')) return [] | |
| if (segments.length === 3) return [`${mountPath}/threads`] | |
| return [] | |
| }) | |
| } | |
| function slackThreadContextMountPathsForIntegration(integration: ConnectedIntegration): string[] { | |
| const provider = toRelayfileProvider(integration.provider) | |
| if (provider !== 'slack' && !provider.startsWith('slack-')) return [] | |
| return canonicalMountPathsForConnectedIntegration(integration) | |
| .filter(isNarrowHistoricalMountPath) | |
| .flatMap((mountPath) => { | |
| const segments = mountPath.split('/').filter(Boolean) | |
| if (segments[0] !== provider) return [] | |
| const collection = segments[1] | |
| if (!['channels', 'dms', 'users'].includes(collection || '')) return [] | |
| if (segments.length === 3) return [mountPath + '/threads'] | |
| return [] | |
| }) | |
| } |
|
ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed. Reviewed PR #154 against Validation run:
All local tests passed. I’m not printing |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/integration-event-bridge.ts`:
- Around line 413-416: The bridge function slackListenDms uses
scopeBooleanDefault(..., false) to make DM listening opt-in, but the UI snippets
still default listenDms to true; update the UI/snippet/default payloads so the
listenDms/listenDirectMessages/directMessages scope defaults to false (matching
slackListenDms and scopeBooleanDefault) — ensure the UI uses the same default
value or the same helper logic when constructing the integration scope so DM
listening is opt-in across both codepaths.
In `@src/main/integration-mounts.test.ts`:
- Around line 279-295: Add a regression test that ensures a single Slack channel
spec expands into both messages and threads mounts when downloadHistoricalData
is false: instantiate IntegrationMountManager, call ensureMounted with a spec
like { provider: 'slack', mountPaths: ['/slack/channels/C123'],
downloadHistoricalData: false } and assert that mock.mountInputs contains two
entries matching remotePath '/slack/channels/C123/messages' and
'/slack/channels/C123/threads' with distinct localDir values and both having
localLayout 'exact' and syncMode 'mirror' (use IntegrationMountManager,
ensureMounted, and mock.mountInputs to locate and verify the mounts).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d9b8f737-bfeb-489b-bb5e-ee94f6757110
📒 Files selected for processing (5)
src/main/__tests__/integration-event-bridge.test.tssrc/main/integration-event-bridge.tssrc/main/integration-mounts.test.tssrc/main/integrations.test.tssrc/main/integrations.ts
| function slackListenDms(integration: ConnectedIntegration): boolean { | ||
| if (!isSlackProvider(integration.provider)) return false | ||
| return scopeBooleanDefault(integration.scope, ['listenDms', 'listenDirectMessages', 'directMessages'], true) | ||
| return scopeBooleanDefault(integration.scope, ['listenDms', 'listenDirectMessages', 'directMessages'], false) | ||
| } |
There was a problem hiding this comment.
Align DM defaults across UI and bridge to keep DM listening truly opt-in.
Line 415 now defaults unset DM scope to false, but the provided UI snippets still default listenDms to true when unset. That mismatch can silently keep DM subscriptions enabled and weakens the PR’s intended privacy posture.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/integration-event-bridge.ts` around lines 413 - 416, The bridge
function slackListenDms uses scopeBooleanDefault(..., false) to make DM
listening opt-in, but the UI snippets still default listenDms to true; update
the UI/snippet/default payloads so the
listenDms/listenDirectMessages/directMessages scope defaults to false (matching
slackListenDms and scopeBooleanDefault) — ensure the UI uses the same default
value or the same helper logic when constructing the integration scope so DM
listening is opt-in across both codepaths.
| it('mounts Slack thread context roots in mirror mode', async () => { | ||
| const manager = new IntegrationMountManager() | ||
|
|
||
| await manager.ensureMounted([ | ||
| { | ||
| provider: 'slack', | ||
| mountPaths: ['/slack/channels/C123/threads'] | ||
| } | ||
| ]) | ||
|
|
||
| expect(mock.mountInputs[0]).toMatchObject({ | ||
| localDir: '/tmp/pear-home/.agentworkforce/pear/relayfile/workspaces/account-workspace-id/slack/channels/C123/threads', | ||
| remotePath: '/slack/channels/C123/threads', | ||
| localLayout: 'exact', | ||
| syncMode: 'mirror' | ||
| }) | ||
| }) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Add a test case for mounting both messages and threads for the same channel.
The coding guideline requires regression tests when touching integration notifications, not just happy-path cases. Based on integrations.test.ts line 490-507, when downloadHistoricalData is false, a single channel spec expands to both /slack/channels/C123/messages and /slack/channels/C123/threads. However, there's no test verifying that mounting both together works correctly as separate mounts without conflicts.
Suggested test case
+ it('mounts both messages and threads for the same channel as separate mounts', async () => {
+ const manager = new IntegrationMountManager()
+
+ await manager.ensureMounted([
+ {
+ provider: 'slack',
+ mountPaths: ['/slack/channels/C123/messages', '/slack/channels/C123/threads']
+ }
+ ])
+
+ expect(mock.mountInputs).toHaveLength(2)
+ expect(mock.mountInputs[0]).toMatchObject({
+ localDir: '/tmp/pear-home/.agentworkforce/pear/relayfile/workspaces/account-workspace-id/slack/channels/C123/messages',
+ remotePath: '/slack/channels/C123/messages',
+ localLayout: 'exact',
+ syncMode: 'write-only'
+ })
+ expect(mock.mountInputs[1]).toMatchObject({
+ localDir: '/tmp/pear-home/.agentworkforce/pear/relayfile/workspaces/account-workspace-id/slack/channels/C123/threads',
+ remotePath: '/slack/channels/C123/threads',
+ localLayout: 'exact',
+ syncMode: 'mirror'
+ })
+ })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/integration-mounts.test.ts` around lines 279 - 295, Add a regression
test that ensures a single Slack channel spec expands into both messages and
threads mounts when downloadHistoricalData is false: instantiate
IntegrationMountManager, call ensureMounted with a spec like { provider:
'slack', mountPaths: ['/slack/channels/C123'], downloadHistoricalData: false }
and assert that mock.mountInputs contains two entries matching remotePath
'/slack/channels/C123/messages' and '/slack/channels/C123/threads' with distinct
localDir values and both having localLayout 'exact' and syncMode 'mirror' (use
IntegrationMountManager, ensureMounted, and mock.mountInputs to locate and
verify the mounts).
Source: Coding guidelines
|
|
||
| function slackListenDms(integration: ConnectedIntegration): boolean { | ||
| if (!isSlackProvider(integration.provider)) return false | ||
| return scopeBooleanDefault(integration.scope, ['listenDms', 'listenDirectMessages', 'directMessages'], true) | ||
| return scopeBooleanDefault(integration.scope, ['listenDms', 'listenDirectMessages', 'directMessages'], false) | ||
| } |
There was a problem hiding this comment.
🚩 Slack DM listening default change is a breaking behavioral change
The slackListenDms function at src/main/integration-event-bridge.ts:403 changes its default from true to false. Any existing integration configuration that omits an explicit listenDms/listenDirectMessages/directMessages scope key will silently stop receiving DM events. This is clearly intentional (test renamed from 'can be disabled' to 'is opt-in'), but downstream consumers or existing project configurations that relied on the implicit default will be affected without any migration path or warning log.
(Refers to lines 403-416)
Was this helpful? React with 👍 or 👎 to provide feedback.
|
Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI. |
…ssion (#155) * fix(slack): local-disk fallback for event preview + suffixed path emission readEventContextPreview() was SDK-read-only (client.readFile), which missed the suffixed mount candidate under remote-consistency lag even when the file was already on local disk. Events therefore emitted bare-path + "content unavailable" on a current build — #154 fixed candidate generation, not the read source. - Add local-disk fallback scoped to matched concrete mount roots only (no broad .integrations scan; preserves DM "watched-but-not-mounted" state) - Emit the resolved suffixed path for visible Path:, data.path, and data.resource.path (closes the structured-metadata-stays-bare gap) - Regression for bare event path + suffixed local mount + SDK read miss Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: apply pr-reviewer fixes for #155 * chore: apply pr-reviewer fixes for #155 --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: agent-relay-code[bot] <agent-relay-code[bot]@users.noreply.github.com>
Summary
C123events can resolveC123__namecontent/threadsroots as narrow mirror context roots so thread replies can reconcile locally without broad channel-history syncCross-repo alignment
go test ./....files.readand path-scope checks apply the same same-channel raw/alias equivalence, deny DMs by default, and pass focused gateway regression tests.Validation
node --experimental-strip-types --no-warnings --test src/main/__tests__/integration-event-bridge.test.ts(61/61)npx vitest run src/main/integrations.test.ts src/main/integration-mounts.test.ts(42/42)npm test -- src/main/__tests__/integration-event-bridge.test.ts(92/92)